查看原文
其他

Patract完成Ask! v0.3开发:正式发布 ask 和 ask-cli 的 npm 库

橙汁 Patract开放平台 2022-03-30

3个月前,Patract 提交了 Kusama 国库的[第#81号]提案,关于 Ask! v0.3 的的实现目标,原理及过程。在那份提案中,我们将在 v0.3 版本中完成以下功能:


v0.3 目标: 提供 ask-cli 管理工具,并优化 Ask! 的编程体验和性能

  • 增加项目管理工具 ask-cli

  • 性能优化

  • 提供自定义 env 中系统参数的类型

  • 单元测试和文档


我们已经实现的源码在 Ask! (https://github.com/patractlabs/ask)项目仓位中, 示例合约在 examples(https://github.com/patractlabs/ask/tree/master/examples)目录下,部分文档在 docs.patract.io,请在 v0.3-review 分支上 review,完成之后将合并到 main 分支。



1设计与实现



Ask! v0.3 在 v0.2 版本的基础上,增加了 ask-cli 命令行工具,用于管理合约开发工作;优化了 Ask! 的性能;并提供了相关的使用文档。


增加项目管理工具 ask-cli


ask-cli 是 Ask! 的命令行管理工具,用来管理合约编译的生命周期。它提供了 init 和 compile 两个功能:


  • ask-cli init 命令:

    • init 命令用于初始化 Ask! 合约项目,它从 Dependencies(https://raw.githubusercontent.com/patractlabs/ask-cli/main/depens.json)列表中读取当前 Ask! 项目的最新配置信息,然后更新对应的 npm 包。然后,它将建立起本地文件目录结构:


.├── build├── contracts├── node_modules└── package.json


其中: contracts 目录下存放合约源码文件,build 目录在执行编译命令之后产生,里面存放生成的 Wasm 和 metadata.json 文件,以及如果是 --debug 模式下生成的其它文件。


  • ask-cli compile [--release|--debug] contracts/Hello.ts 命令:

    • compile 命令于编译指定的合约文件, 并将生成的 Wasm 和 metadata.json 文件存放于 build 目录下。

    • 它有两种模式,--release 是默认选项,编译器将使用最高级别的优化和压缩;--debug 是调用模式,除了 Wasm 和 metadata.json 文件外,还会生成编译过程中产生的文件。


关于 ask-cli 的详细使用说明,请参考 QuickStart(https://github.com/patractlabs/ask/blob/v0.3-dev/docs/Quickstart.md)中对应的章节。


性能优化


2.1 合并 @storage 的功能到 @contract 中,减化状态变量定义的流程。


在 v0.3 之前的版本中,合约的状态变量是单独定义在由 @storage 注解的类里面。这种实现方式,不能很好的支持合约继承时,状态变量不便于统一安排存储位置。所以在 v0.3 版本中,我们将状态变量直接定义在合约中,并废除了 @storage 注解,同时引入了 @state 注解,用于标记合约中的成员变量是状态变量,未被 @state 标注的成员变量,则是普通的类成员变量。


  • 为满足条款2.2的要求,当合约使用继承时,所有@state标注的状态变量,将按照它们的声明顺序,从基类到子类的顺序,进行排序,它们排序序号,将作为状态变量存储位置的编号。

  • 编译器首先定位合约入口。


this.program.elementsByName.forEach((element, _) => { let contractNum = 0; if (ElementUtil.isTopContractClass(element)) { contractNum++; this.contract = new ContractInterpreter(<ClassPrototype>element); } });


然后递归父类把要存储的对象入栈。


private resolveBaseClass(classPrototype: ClassPrototype): void { if (classPrototype.basePrototype) { let basePrototype = classPrototype.basePrototype; basePrototype.instanceMembers && basePrototype.instanceMembers.forEach((instance, _) => { if (ElementUtil.isField(instance)) { let fieldDef = new FieldDef(<FieldPrototype>instance); if (!fieldDef.decorators.ignore) { this.storeFields.push(fieldDef); } } }); this.resolveBaseClass(basePrototype); }}


为满足条款2.3的要求,@state 中引入了选项 lazy,即: @state({"lazy": false}) 方式。


  • 当 lazy 为 true 时,表示这个状态变量在同一次调用过程中,多次改变它的值时,只有最后一次的改变会同步到链上;lazy的默认值为true。

  • 当 lazy 为 false 时,则表示每一次改变这个状态变量的值,都会立即同步到链上。


它的基本实现原理是:

  • 对于每一个 lazy 为 true 的状态变量, 编译器给它们生成的 setter 方法, 将只更新内存中的值;同时,编译器将为合约生成一个__commit__方法,在这个方法中,这些状态变量的值如果曾经发生过改变,则将它们最新的值同步到链上。

  • 而对于 lazy 为 false 的状态变量, 编译器生成的 setter 方法, 除了更新内存中的值,也会立即同步到链上。编译器会根据存储对象的状态,生成不同的 setter。


以 bool 类型举例, private vbool: bool;当 lazy 是 false 时, 其生成的扩展 set 方法如下;


set vbool(newvalue: bool) { this._vbool = new Bool(newvalue); const st = new Storage(new Hash("0x0000000000000000000000000000000000000000000000000000000000000001")); st.store<Bool>(this._vbool!); }


当 lazy 为 true 时,其生成的扩展方法如下:

set vbool(v: bool) { this._vbool = new _lang.Bool(v); } __commit_storage__(): void { if (this._vbool !== null) { const st = new _lang.Storage(new _lang.Hash([0x0000000000000000000000000000000000000000000000000000000000000001])); st.store<_lang.Bool>(this._vbool!); }
}


2.2 优化状态变量存储时使用的 key 生成逻辑: 使用连续的 hash 数据,
代替动态的 hash(string) 方式。见条款 2.1。


2.3 在一个 message 调用过程中,多次改变状态变量的值时,减少seal_set_storage的调用次数。见条款 2.1.

2.4 定义 Map 和 Array 在 metadata.json 中导出格式。


Array 的的导出格式分成两部分,type 定义和 store 定义。在 Ask! 里面 Array 是默认变长数组,默认定义其结构为 SequenceDef 如下标记 array 为sequence,同时指定 Array 对象存储对象的类型,以及存储模式是 pack 还是 spread。对于 Array 还可以默认分配一些空间,他生成的 type 为 Arraydef 包含分配空间 capacity 的大小。

export interface Type extends ToMetadata { typeKind(): TypeKind;
toMetadata(): ITypeDef;}
export class SequenceDef implements Type { constructor(public readonly type: number) {}
typeKind(): TypeKind { return TypeKind.Sequence; }
toMetadata(): ISequenceDef { return { def: { sequence: { type: this.type, }, }, }; }}
export class ArrayDef implements Type { constructor(public readonly len: number, public readonly type: number) {}
typeKind(): TypeKind { return TypeKind.Array; }
toMetadata(): IArrayDef { return { def: { array: { len: this.len, type: this.type, }, }, }; }}

SequenceDef 生成结构示例如下:

{ "def": { "sequence": { "type": 4 } } }

ArrayDef 生成结构示例如下:

{ "def": { "array": { "len": 32, "type": 2 } } }


他们存储结构示例如下,

{ "name": "ages", "layout": { "struct": { "fields": [ { "name": "len", "layout": { "key": "0x0000000000000000000000000000000000000000000000000000000000000002", "ty": 3 } }, { "name": "elems", "layout": { "offset": "0x0000000000000000000000000000000000000000000000000000000000000002", "len": 0, "cellsPerElem": 1, "layout": { "key": "0x0000000000000000000000000000000000000000000000000000000000000002", "ty": 3 }, "storemode": "spread" } } ] } }}


其中区别为:ArrayDef 包含 capacity 信息的 len 字段不为 0SequenceDef 的为0。


{ "name": "elems", "layout": { "offset": "0x0000000000000000000000000000000000000000000000000000000000000002", "len": 0, "cellsPerElem": 1, "layout": { "key": "0x0000000000000000000000000000000000000000000000000000000000000002", "ty": 3 }, "storemode": "spread"}


对于 map 存储对象解析,需要知道存储的对象 key 和 value 的类型,以及存储模式是 pack 和 spread。还有存储对象入口 key 的值。


export class CompositeDef implements Type { constructor(public readonly fields: Array<Field>) {}
typeKind(): TypeKind { return TypeKind.Composite; } toMetadata(): ICompositeDef { return { def: { composite: { fields: this.fields.map((f) => f.toMetadata()), }, }, }; }}


其生成结构实例如下:


{ "def": { "composite": { "fields": [ { "name": "key_index", "type": 2 }, { "name": "value", "type": 3 } ] } } }, { "def": { "primitive": "u8" } }, { "def": { "primitive": "str" } }


其存储结构如下:


{ "name": "allowances", "layout": { "struct": { "fields": [ { "name": "key", "layout": { "offset": "0x0000000000000000000000000000000000000000000000000000000000000002", "strategy": { "hasher": "Blake2x256", "prefix": "0x0000000000000000000000000000000000000000000000000000000000000002", "postfix": "" }, "layout": { "key": "0x0000000000000000000000000000000000000000000000000000000000000002", "ty": 3 }, "storemode": "spread" } }, { "name": "values", "layout": { "offset": "0x0000000000000000000000000000000000000000000000000000000000000002", "strategy": { "hasher": "Blake2x256", "prefix": "0x0000000000000000000000000000000000000000000000000000000000000002", "postfix": "" }, "layout": { "key": "0x0000000000000000000000000000000000000000000000000000000000000002", "ty": 6 }, "storemode": "spread" } } ] } }}


2.5 使用 JSON 的方式代替()的注解方式


在 v0.3 之前的版本中,注解的选项,是以 @message(selector = '0x00001111') 的方式提供的,这种实现方式不是很直观,而且不符合编码习惯,所以在 v0.3 中,我们把它改成以下方式:


@message({"selector": "0x00001111"})


这种方式的可读性比较好,同时,也便于编译器解析处理。


先获取注解的参数部分比如“{"selector": "0x00001111"}”解析成 json 对象


export class DecoratorNodeDef { jsonObj: any; constructor(public decorator: DecoratorNode) { this.jsonObj = this.parseToJson(decorator); }}


针对特定注解创建特定的注解类,并做特定的检查。


export class MessageDecoratorNodeDef extends DecoratorNodeDef { constructor(decorator: DecoratorNode, public payable = false, public mutates = true, public selector = "") { super(decorator); this.payable = this.getIfAbsent("payable", false, "boolean"); this.mutates = this.getIfAbsent('mutates', true, "boolean"); if (this.hasProperty('selector')) { this.selector = this.getProperty('selector'); DecoratorUtil.checkSelector(decorator, this.selector); } if (this.payable && !this.mutates) { throw new Error(`Decorator: ${decorator.name.range.toString()} arguments mutates and payable can only exist one. Trace: ${RangeUtil.location(decorator.range)} `); } }}


2.6 增强 Event 语法


在 v0.2 中, 我们引入了 @event 注解,用来发出一个事件。但是在这个版本中,Event 不能被继承,而且定义 Event 就默认 emit 的实现方法,也不是很直观。所以在 v0.3 版本中,我们对Event做了以下增强和优化:


  • 实现 @event 类时, 需要明确是从__lang.Event 继承, 或者继承于另一个 Event。

  • 需要明确调用 emit() 方法来发出事件。


新的 Event 使用方法示例如下:


@eventclass EventA extends __lang.Event {
@topic topicA: u8; name: string;
constructor(t: u8, n: string) { super(); this.topicA = t; this.name = n; } }
@eventclass EventB extends EventA { @topic topicB: u8; gender: string; constructor(t: u8, g: string) { super(t, g); this.topicB = t; this.gender = g; }}
@contractexport class EventEmitter {
count: i8;
constructor() { }
@message triggeEventA(): void { let eventA = new EventA(100, "Elon"); eventA.emit(); }
@message triggeEventB(): void { let eventB = new EventB(<u8>300, "M"); eventB.emit(); }
}


2.7 增强注解的语法和参数检查


之前的版本,只会提示错误的注解,比如 @massage,会提示合约不支持注解@massge。


@massage({"mutates": false}) get(): bool { return this.flag; }


增强检查后,会通过匹配符算法,猜测用户要输入"@message"的注解,给出提示,如下:

Unsupported contract decorator @massage, do you mean '@message'? Check source text: @massage({"mutates": false}) in path:examples/flipper/flipper.ts lineAt: 24 columnAt: 5 range: (346 374).


还会检查 @message 标记的是不是 public 方法。

Decorator[@message] should mark on public method(Method: get isn't public method). Check source text: @message({"mutates": false}) @message({"mutates": false}) private get(): bool { return this.flag;    } in path:examples/flipper/flipper.ts lineAt: 24 columnAt: 5 range: (346 432)..


会提示错误信息:

Decorator[@message] should mark on public method(Method: get isn't public method). Check source text: @message({"mutates": false}) private get(): bool { return this.flag;
} in path:examples/flipper/flipper.ts lineAt: 24 columnAt: 5 range: (346 432)..


2.8 优化生成的 Wasm 文件大小


v0.3 版本中,ask-cli 的默认使用 --release 编译模式, 编译器将使用 -o3z 选

项来优化及压缩生成的 Wasm 文件;同时在 Framework 框架中,减少使用

字符串资源,以减小 Framework 的代码量。


2.9 升级 pallet-contract 中的 seal_xxx 方法


合约中的使用到的 seal_xxx 方法,已经更新到基于 Europa(https://github.com/patractlabs/europa)的最新 seal0 版本。


提供自定义 env 中系统参数的类型的能力。


在 Ask! 编程环境中,以下四个数据类型是可以根据运行的 FRAME 进行定制的: AccountId,Hash,Balance,BlockNumber。


它们的 Ask! 中的默认值分别为: Array<u8>(32), Array<u8>(32), UInt128,UInt32。


如果需要重新定义它们的实现方式,请在 Ask! 的项目路径 assembly/env/CustomTypes.ts 中, 重新实现即可。Ask! 对于它们的要求是: 它们都需要实现 Codec,此外没有其它的要求。


单元测试和文档


在 v0.3 版本中,我们提供了 examples/ 用来在线测试 Framework 的功能。提供了__tests__/ 用来测试编译器的功能ts-package

下面有 ts-packages/contract-metadata/src/ 和ts-packages/transform/src/__tests__/ 目录用来放测试用例。


我们提供的文档,请参考使用 Ask! v0.3 章节。



2使用 Ask! v0.3



Ask! v0.3 已经正式发布,请参考我们的 QuickStart(https://github.com/patractlabs/ask/blob/v0.3-dev/docs/Quickstart.md)。关于 Ask! 中各组件的使用文档,请参考 API 手册(https://github.com/patractlabs/ask/blob/v0.3-dev/docs/api/index.html)。


操作说明


现在我们可以使用 ask-cli 来编写 Ask! 智能合约了。


1.新建一个目录: mkdir erc20

2.进入新目录: cd erc20

3.新建一个 npm 项目: npm init -y

4.安装 pl-ask-cli: npm i pl-ask-cli

5.初始化项目: npx ask-cli init

6.拷贝 example/erc20(https://github.com/patractlabs/ask/tree/master/examples/erc20)下面的 index.ts,ERC20.ts 文件到 contracts/ 目录下。

7.编译: npx ask-cli compile contracts/index.ts


编译成功之后,我们就可以执行部署和调用的操作了。


使用 ERC20 合约


ERC20.ts 是一个符合 ERC20 标准的基类,它封装了可重复使用的 ERC20 接口,如 transfer,approve 等。定义了合约使用到的存储结构,以及事件 Transfer 和 Approval。在 Ask! v0.3中,我们已经按新的编程规范,重新实现了ERC20(https://github.com/patractlabs/ask/tree/master/examples/erc20),所以我们仍然可以这样写自己的 Token 合约:


import { Account, u128 } from "ask-lang";import {ERC20} from "./ERC20";
@contract@doc({"desc": "MyToken conract that extended erc20 contract"})class MyToken extends ERC20 {
constructor() { super(); }
@constructor default(name: string = "", symbol: string = ""): void { super.default(name, symbol); }

@message @doc({"desc": "Mint a token"}) mint(to: Account, amount: u128): void { this._mint(to, amount); }
@message @doc({"desc": "burn the token"}) burn(from: Account, amount: u128): void { this._burn(from, amount); }
}


编译合约


使用以下的命令来编译我们的合约:


$ npx ask-cli compile contracts/index.ts


编译成功之后,将会在 examples/erc20/build/ 目录下生成 Wasm 和 metadata.json 文件。


部署和调用


我们在 Europa(https://github.com/patractlabs/europa)的 v3.0.0 分支沙盒环境中部署和测试合约功能,前端使用 polkadot-js(https://github.com/polkadot-js/apps),基于 master 分支,commit-id 11276477a0523348c7b143db566622aa32833296 代码基础,作为前端交互界面。


测试步骤如下:


1.首先我们按照 Europa 和 plokadot-js 的说明,启动节点和服务。


2.在 polkadot-js 的合约界面中,上传 build/ 下的 metadata.json 和 target.wasm 文件。


3.部署已经上传的合约,调用 default 方法发行 Token。


4.调用 mint, transfer, approve, burn 等方法,操作 ERC20 Token。


(视频链接)https://user-images.githubusercontent.com/2844215/120952438-9cc61a00-c77d-11eb-9745-454f977184be.gif


至此,在使用 ask-cli 和新的合约模型下,发行了 ERC20 通证。



3Ask! v0.3 已经实现的内容:


  • 正式发布 ask 和 ask-cli 的 npm 库。

  • 用新的 contract 实现方式重新实现 examples 中的合约。

  • 完整的合约开发教程。

  • 完整的 API 文档。




往期精彩:2021 Web3.0 BootCamp|历经5个月,Patract所有产品线整体推进71%,继续砥砺深耕Wasm合约技术
2021 Sub0 Online精彩回顾|Patract Labs的Wasm智能合约
ink! 开发实践—组合范式(Metis) 之 ERC721及 ReceiverWasm合约测试网Jupiter已发布平行链版本Wasm合约生态的合约编程实践

About Patract
Patract 为波卡 Wasm 合约生态的平行链和 DApp 开发提供解决方案。
How to join Patract官网|https://patract.ioElement|https://app.element.io/#/room/#PatractLabsDev:matrix.orgDiscord|https://discord.gg/znbmjYfvBRPatract 开放平台|https://open.patract.io
Telegram|https://t.me/patract
Twitter|https://twitter.com/PatractLabs
我们正招聘区块链开发工程师、前端/全栈开发工程师、云平台架构师、数据产品经理、产品经理等岗位,可以联系 sean@patract.io
扫码加入Patract微信开发群

您可能也对以下帖子感兴趣

文章有问题?点此查看未经处理的缓存